/*
 *
 *  *
 *  *   Copyright 2020-2021 Luter.me
 *  *
 *  *   Licensed under the Apache License, Version 2.0 (the "License");
 *  *   you may not use this file except in compliance with the License.
 *  *   You may obtain a copy of the License at
 *  *
 *  *   http://www.apache.org/licenses/LICENSE-2.0
 *  *
 *  *   Unless required by applicable law or agreed to in writing, software
 *  *   distributed under the License is distributed on an "AS IS" BASIS,
 *  *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  *   See the License for the specific language governing permissions and
 *  *   limitations under the License.
 *  *
 *
 */

package com.iocup.keybastion.utils;

import org.apache.commons.lang3.StringUtils;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 路径匹配工具
 */
public class PathUtil {
    /**
     * The constant EMPTY_STRING_ARRAY.
     */
    public static final String[] EMPTY_STRING_ARRAY = new String[0];
    /**
     * The constant DEFAULT_PATH_SEPARATOR.
     */
    public static final String DEFAULT_PATH_SEPARATOR = "/";
    /**
     * The constant CACHE_TURNOFF_THRESHOLD.
     */
    private static final int CACHE_TURNOFF_THRESHOLD = 65536;
    /**
     * The constant WILDCARD_CHARS.
     */
    private static final char[] WILDCARD_CHARS = {'*', '?', '{'};
    /**
     * The constant ASTERISK.
     */
    private static final String ASTERISK = "*";
    /**
     * The Path separator.
     */
    private final String pathSeparator;
    /**
     * The Case sensitive.
     */
    private final boolean caseSensitive = true;
    /**
     * The Trim tokens.
     */
    private final boolean trimTokens = false;
    /**
     * The Cache patterns.
     */
    private volatile Boolean cachePatterns;
    /**
     * The Tokenized pattern cache.
     */
    private final Map<String, String[]> tokenizedPatternCache = new ConcurrentHashMap<>(256);
    /**
     * The String matcher cache.
     */
    final Map<String, PathStringMatcher> stringMatcherCache = new ConcurrentHashMap<>(256);

    /**
     * Instantiates a new Path util.
     */
    public PathUtil() {
        this.pathSeparator = DEFAULT_PATH_SEPARATOR;
    }

    public static String generateKey(String methodType, String url) {
        if (StringUtils.isBlank(methodType)) {
            return DEFAULT_PATH_SEPARATOR + "**" + url;
        }
        return DEFAULT_PATH_SEPARATOR + methodType + url;
    }

    /**
     * Is match boolean.
     *
     * @param patterns the patterns
     * @param path     the path
     * @return the boolean
     */
    public boolean isMatch(List<String> patterns, String path) {
        if (null == patterns || patterns.isEmpty() || StringUtils.isBlank(path)) {
            return false;

        }
        for (String pattern : patterns) {
            if (StringUtils.isNotBlank(pattern) && match(pattern, path)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 所有 paths 都不合法
     * <p>
     * path 为空，或者不是/开头的
     *
     * @param paths the paths
     */
    public static void isInValidPath(String... paths) {
        for (String string : paths) {
            if (isValidPath(string)) {
                throw new IllegalArgumentException("error: path [" + string + "] is illegal ");
            }
        }
    }

    /**
     * 所有的 paths  都合法
     * <p>
     * path 不为空，且以/开头
     *
     * @param paths the paths
     */
    public static void isValidPath(String... paths) {
        for (String string : paths) {
            if (isInValidPath(string)) {
                throw new IllegalArgumentException("error: path [" + string + "] is illegal ");
            }
        }
    }

    /**
     * path 不合法
     * <p>
     * path 为空，或者不是/开头的
     *
     * @param path the path
     * @return the boolean
     */
    public static boolean isInValidPath(String path) {
        return StringUtils.isBlank(path) || !path.startsWith(PathUtil.DEFAULT_PATH_SEPARATOR);
    }

    /**
     * path 合法
     * <p>
     * path 不为空，且以/开头
     *
     * @param path the path
     * @return the boolean
     */
    public static boolean isValidPath(String path) {
        return StringUtils.isNotBlank(path) && path.startsWith(PathUtil.DEFAULT_PATH_SEPARATOR);
    }

    /**
     * Deactivate pattern cache.
     */
    private void deactivatePatternCache() {
        this.cachePatterns = false;
        this.tokenizedPatternCache.clear();
        this.stringMatcherCache.clear();
    }

    /**
     * Match boolean.
     *
     * @param pattern the pattern
     * @param path    the path
     * @return the boolean
     */
    public boolean match(String pattern, String path) {
        return doMatch(pattern, path);
    }

    /**
     * Do match boolean.
     *
     * @param pattern the pattern
     * @param path    the path
     * @return the boolean
     */
    protected boolean doMatch(String pattern, String path) {
        if (StringUtils.isBlank(pattern) || path == null || path.startsWith(this.pathSeparator) != pattern.startsWith(this.pathSeparator)) {
            return false;
        }
        String[] pantDirs = tokenizePattern(pattern);
        if (this.caseSensitive && !isPotentialMatch(path, pantDirs)) {
            return false;
        }
        String[] pathDirs = tokenizePath(path);
        int pattIdxStart = 0;
        int pattIdxEnd = pantDirs.length - 1;
        int pathIdxStart = 0;
        int pathIdxEnd = pathDirs.length - 1;
        // Match all elements up to the first **
        while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) {
            String pattDir = pantDirs[pattIdxStart];
            if ("**".equals(pattDir)) {
                break;
            }
            if (matchStrings(pattDir, pathDirs[pathIdxStart])) {
                return false;
            }
            pattIdxStart++;
            pathIdxStart++;
        }
        if (pathIdxStart > pathIdxEnd) {
            // Path is exhausted, only match if rest of pattern is * or **'s
            if (pattIdxStart > pattIdxEnd) {
                return (pattern.endsWith(this.pathSeparator) == path.endsWith(this.pathSeparator));
            }
            if (pattIdxStart == pattIdxEnd && ASTERISK.equals(pantDirs[pattIdxStart]) && path.endsWith(this.pathSeparator)) {
                return true;
            }
            for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
                if (!"**".equals(pantDirs[i])) {
                    return false;
                }
            }
            return true;
        } else if (pattIdxStart > pattIdxEnd) {
            // String not exhausted, but pattern is. Failure.
            return false;
        }
        // up to last '**'
        while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) {
            String pattDir = pantDirs[pattIdxEnd];
            if ("**".equals(pattDir)) {
                break;
            }
            if (matchStrings(pattDir, pathDirs[pathIdxEnd])) {
                return false;
            }
            pattIdxEnd--;
            pathIdxEnd--;
        }
        if (pathIdxStart > pathIdxEnd) {
            // String is exhausted
            for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
                if (!"**".equals(pantDirs[i])) {
                    return false;
                }
            }
            return true;
        }
        while (pattIdxStart != pattIdxEnd && pathIdxStart <= pathIdxEnd) {
            int patIdxTmp = -1;
            for (int i = pattIdxStart + 1; i <= pattIdxEnd; i++) {
                if ("**".equals(pantDirs[i])) {
                    patIdxTmp = i;
                    break;
                }
            }
            if (patIdxTmp == pattIdxStart + 1) {
                // '**/**' situation, so skip one
                pattIdxStart++;
                continue;
            }
            // Find the pattern between padIdxStart & padIdxTmp in str between
            // strIdxStart & strIdxEnd
            int patLength = (patIdxTmp - pattIdxStart - 1);
            int strLength = (pathIdxEnd - pathIdxStart + 1);
            int foundIdx = -1;

            strLoop:
            for (int i = 0; i <= strLength - patLength; i++) {
                for (int j = 0; j < patLength; j++) {
                    String subPat = pantDirs[pattIdxStart + j + 1];
                    String subStr = pathDirs[pathIdxStart + i + j];
                    if (matchStrings(subPat, subStr)) {
                        continue strLoop;
                    }
                }
                foundIdx = pathIdxStart + i;
                break;
            }

            if (foundIdx == -1) {
                return false;
            }

            pattIdxStart = patIdxTmp;
            pathIdxStart = foundIdx + patLength;
        }

        for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
            if (!"**".equals(pantDirs[i])) {
                return false;
            }
        }

        return true;
    }

    /**
     * Is potential match boolean.
     *
     * @param path     the path
     * @param pattDirs the patt dirs
     * @return the boolean
     */
    private boolean isPotentialMatch(String path, String[] pattDirs) {
        if (!this.trimTokens) {
            int pos = 0;
            for (String pattDir : pattDirs) {
                int skipped = skipSeparator(path, pos, this.pathSeparator);
                pos += skipped;
                skipped = skipSegment(path, pos, pattDir);
                if (skipped < pattDir.length()) {
                    return (skipped > 0 || (pattDir.length() > 0 && isWildcardChar(pattDir.charAt(0))));
                }
                pos += skipped;
            }
        }
        return true;
    }

    /**
     * Skip segment int.
     *
     * @param path   the path
     * @param pos    the pos
     * @param prefix the prefix
     * @return the int
     */
    private int skipSegment(String path, int pos, String prefix) {
        int skipped = 0;
        for (int i = 0; i < prefix.length(); i++) {
            char c = prefix.charAt(i);
            if (isWildcardChar(c)) {
                return skipped;
            }
            int currPos = pos + skipped;
            if (currPos >= path.length()) {
                return 0;
            }
            if (c == path.charAt(currPos)) {
                skipped++;
            }
        }
        return skipped;
    }

    /**
     * Skip separator int.
     *
     * @param path      the path
     * @param pos       the pos
     * @param separator the separator
     * @return the int
     */
    private int skipSeparator(String path, int pos, String separator) {
        int skipped = 0;
        while (path.startsWith(separator, pos + skipped)) {
            skipped += separator.length();
        }
        return skipped;
    }

    /**
     * Is wildcard char boolean.
     *
     * @param c the c
     * @return the boolean
     */
    private boolean isWildcardChar(char c) {
        for (char candidate : WILDCARD_CHARS) {
            if (c == candidate) {
                return true;
            }
        }
        return false;
    }

    /**
     * Tokenize pattern string [ ].
     *
     * @param pattern the pattern
     * @return the string [ ]
     */
    protected String[] tokenizePattern(String pattern) {
        String[] tokenized = null;
        Boolean cachePatterns = this.cachePatterns;
        if (cachePatterns == null || cachePatterns) {
            tokenized = this.tokenizedPatternCache.get(pattern);
        }
        if (tokenized == null) {
            tokenized = tokenizePath(pattern);
            if (cachePatterns == null && this.tokenizedPatternCache.size() >= CACHE_TURNOFF_THRESHOLD) {
                // Try to adapt to the runtime situation that we're encountering:
                // There are obviously too many different patterns coming in here...
                // So let's turn off the cache since the patterns are unlikely to be reoccurring.
                deactivatePatternCache();
                return tokenized;
            }
            if (cachePatterns == null || cachePatterns) {
                this.tokenizedPatternCache.put(pattern, tokenized);
            }
        }
        return tokenized;
    }

    /**
     * Tokenize path string [ ].
     *
     * @param path the path
     * @return the string [ ]
     */
    protected String[] tokenizePath(String path) {
        return tokenizeToStringArray(path, this.pathSeparator, this.trimTokens, true);
    }

    /**
     * Is empty boolean.
     *
     * @param collection the collection
     * @return the boolean
     */
    public static boolean isEmpty(Collection<?> collection) {
        return (collection == null || collection.isEmpty());
    }

    /**
     * To string array string [ ].
     *
     * @param collection the collection
     * @return the string [ ]
     */
    public static String[] toStringArray(Collection<String> collection) {
        return (!isEmpty(collection) ? collection.toArray(EMPTY_STRING_ARRAY) : EMPTY_STRING_ARRAY);
    }

    /**
     * Tokenize to string array string [ ].
     *
     * @param str               the str
     * @param delimiters        the delimiters
     * @param trimTokens        the trim tokens
     * @param ignoreEmptyTokens the ignore empty tokens
     * @return the string [ ]
     */
    public static String[] tokenizeToStringArray(
            String str, String delimiters, boolean trimTokens, boolean ignoreEmptyTokens) {

        if (str == null) {
            return EMPTY_STRING_ARRAY;
        }

        StringTokenizer st = new StringTokenizer(str, delimiters);
        List<String> tokens = new ArrayList<>();
        while (st.hasMoreTokens()) {
            String token = st.nextToken();
            if (trimTokens) {
                token = token.trim();
            }
            if (!ignoreEmptyTokens || token.length() > 0) {
                tokens.add(token);
            }
        }
        return toStringArray(tokens);
    }

    /**
     * Match strings boolean.
     *
     * @param pattern the pattern
     * @param str     the str
     * @return the boolean
     */
    private boolean matchStrings(String pattern, String str) {

        return !getStringMatcher(pattern).matchStrings(str, null);
    }

    /**
     * Gets string matcher.
     *
     * @param pattern the pattern
     * @return the string matcher
     */
    protected PathStringMatcher getStringMatcher(String pattern) {
        PathStringMatcher matcher = null;
        Boolean cachePatterns = this.cachePatterns;
        if (cachePatterns == null || cachePatterns) {
            matcher = this.stringMatcherCache.get(pattern);
        }
        if (matcher == null) {
            matcher = new PathStringMatcher(pattern, this.caseSensitive);
            if (cachePatterns == null && this.stringMatcherCache.size() >= CACHE_TURNOFF_THRESHOLD) {
                // Try to adapt to the runtime situation that we're encountering:
                // There are obviously too many different patterns coming in here...
                // So let's turn off the cache since the patterns are unlikely to be reoccurring.
                deactivatePatternCache();
                return matcher;
            }
            if (cachePatterns == null || cachePatterns) {
                this.stringMatcherCache.put(pattern, matcher);
            }
        }
        return matcher;
    }

    /**
     * The type Path string matcher.
     *
     * @author luter
     */
    protected static class PathStringMatcher {

        /**
         * The constant GLOB_PATTERN.
         */
        private static final Pattern GLOB_PATTERN = Pattern.compile("\\?|\\*|\\{((?:\\{[^/]+?}|[^/{}]|\\\\[{}])+?)}");

        /**
         * The constant DEFAULT_VARIABLE_PATTERN.
         */
        private static final String DEFAULT_VARIABLE_PATTERN = "((?s).*)";

        /**
         * The Raw pattern.
         */
        private final String rawPattern;

        /**
         * The Case sensitive.
         */
        private final boolean caseSensitive;

        /**
         * The Exact match.
         */
        private final boolean exactMatch;

        /**
         * The Pattern.
         */
        private final Pattern pattern;

        /**
         * The Variable names.
         */
        private final List<String> variableNames = new ArrayList<>();

        /**
         * Instantiates a new Path string matcher.
         *
         * @param pattern       the pattern
         * @param caseSensitive the case sensitive
         */
        public PathStringMatcher(String pattern, boolean caseSensitive) {
            this.rawPattern = pattern;
            this.caseSensitive = caseSensitive;
            StringBuilder patternBuilder = new StringBuilder();
            Matcher matcher = GLOB_PATTERN.matcher(pattern);
            int end = 0;
            while (matcher.find()) {
                patternBuilder.append(quote(pattern, end, matcher.start()));
                String match = matcher.group();
                if ("?".equals(match)) {
                    patternBuilder.append('.');
                } else if ("*".equals(match)) {
                    patternBuilder.append(".*");
                } else if (match.startsWith("{") && match.endsWith("}")) {
                    int colonIdx = match.indexOf(':');
                    if (colonIdx == -1) {
                        patternBuilder.append(DEFAULT_VARIABLE_PATTERN);
                        this.variableNames.add(matcher.group(1));
                    } else {
                        String variablePattern = match.substring(colonIdx + 1, match.length() - 1);
                        patternBuilder.append('(');
                        patternBuilder.append(variablePattern);
                        patternBuilder.append(')');
                        String variableName = match.substring(1, colonIdx);
                        this.variableNames.add(variableName);
                    }
                }
                end = matcher.end();
            }
            // No glob pattern was found, this is an exact String match
            if (end == 0) {
                this.exactMatch = true;
                this.pattern = null;
            } else {
                this.exactMatch = false;
                patternBuilder.append(quote(pattern, end, pattern.length()));
                this.pattern = (this.caseSensitive ? Pattern.compile(patternBuilder.toString()) :
                        Pattern.compile(patternBuilder.toString(), Pattern.CASE_INSENSITIVE));
            }
        }

        /**
         * Quote string.
         *
         * @param s     the s
         * @param start the start
         * @param end   the end
         * @return the string
         */
        private String quote(String s, int start, int end) {
            if (start == end) {
                return "";
            }
            return Pattern.quote(s.substring(start, end));
        }

        /**
         * Match strings boolean.
         *
         * @param str                  the str
         * @param uriTemplateVariables the uri template variables
         * @return the boolean
         */
        public boolean matchStrings(String str, Map<String, String> uriTemplateVariables) {
            if (this.exactMatch) {
                return this.caseSensitive ? this.rawPattern.equals(str) : this.rawPattern.equalsIgnoreCase(str);
            } else if (this.pattern != null) {
                Matcher matcher = this.pattern.matcher(str);
                if (matcher.matches()) {
                    if (uriTemplateVariables != null) {
                        if (this.variableNames.size() != matcher.groupCount()) {
                            throw new IllegalArgumentException("The number of capturing groups in the pattern segment " +
                                    this.pattern + " does not match the number of URI template variables it defines, " +
                                    "which can occur if capturing groups are used in a URI template regex. " +
                                    "Use non-capturing groups instead.");
                        }
                        for (int i = 1; i <= matcher.groupCount(); i++) {
                            String name = this.variableNames.get(i - 1);
                            String value = matcher.group(i);
                            uriTemplateVariables.put(name, value);
                        }
                    }
                    return true;
                }
            }
            return false;
        }

    }

}
